package lelouet.sh3d.plugins.sweetDatacenter.model;

import gridlib.api.Cluster;
import gridlib.api.Grid;
import gridlib.api.GridElement;
import gridlib.api.IllegalLifeCycleException;
import gridlib.api.Level;
import gridlib.api.Machine;
import gridlib.api.MachineModel;
import gridlib.api.Rack;
import gridlib.api.Site;
import gridlib.api.VM;
import gridlib.persistence.GridConfigurationGenerator;

import java.io.File;
import java.io.FileWriter;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;

import lelouet.datamap.DataMap;
import lelouet.datamap.DataMapFactory;
import lelouet.datamap.ServerPoint;

import com.eteks.sweethome3d.model.CatalogPieceOfFurniture;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.HomePieceOfFurniture;
import com.eteks.sweethome3d.model.Selectable;

/**
 * handles the links between the {@link GridElement}s and the
 * {@link CatalogPieceOfFurniture}s<br />
 * This class aims at
 * <ul>
 * <li>tracing the modifications</li>
 * <li>managing positions and constraints</li>
 * <li>saving models</li>
 * </ul>
 */
public abstract class GridGraphicalBridge implements Serializable {

	private static final long serialVersionUID = 1L;

	/** the grid in which we add {@link GridElement}s */
	protected Grid grid;

	protected HashSet<GServer> gservers = new HashSet<GServer>();

	protected HashSet<GRack> gracks = new HashSet<GRack>();

	/** the home in which {@link HomePieceOfFurniture} are added */
	protected Home home;

	public GridGraphicalBridge(final Home home) {
		this.home = home;
	}

	/** deletes everything in the grid */
	public void clear() {
		try {
			clearDataMap();
			lelouet.sh3d.Utils.clearHomeFurniture(getHome());
			lelouet.sh3d.Utils.clearWalls(getHome());
			gracks.clear();
			gservers.clear();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	protected Set<GRack> getGRacks() {
		return gracks;
	}

	protected Set<GServer> getGServers() {
		return gservers;
	}

	protected Home getHome() {
		return home;
	}

	public String describe() {
		final StringBuilder ret = new StringBuilder("racks:\n");
		for (final GRack r : getGRacks()) {
			ret.append(r.describe(""));
		}
		ret.append("\nservers:\n");
		for (final GServer s : getGServers()) {
			ret.append(s.getName());
			ret.append(" in ");
			final GRack owner = s.getOwner();
			ret.append(owner == null ? "no Rack" : owner.getName());
			ret.append("\n");
		}
		return ret.toString();
	}

	/**
	 * @return are the two racks position specified too close ? the args are the
	 *         x, y pos of center, width, depth, angle of both racks
	 */
	public static boolean areTooclose(float x0, float y0, float w0, float d0,
			float a0, float x1, float y1, float w1, float d1, float a1) {
		float min1 = Math.min(w0, d0);
		float min2 = Math.min(w1, d1);
		float dist = (float) Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0)
				* (y1 - y0));
		return dist <= (min1 + min2) / 2;
	}

	/**
	 * @return a new position to place a rack of given width, depth and angle,
	 *         with one rack to avoid.
	 */
	public static float newPosX(float w0, float d0, float a0, float x1,
			float y1, float w1, float d1, float a1) {
		float min1 = Math.min(w0, d0);
		float min2 = Math.min(w1, d1);
		return x1 + (min1 + min2) / 2 + 1;
	}

	/** find a correct (x, y) position to put the GRack and put it there */
	public void placeGRack(GRack grack) {
		boolean modified = true;
		float posX = grack.getX();
		float posY = grack.getY();
		while (modified) {
			modified = false;
			for (final GRack otherGrack : gracks) {
				if (otherGrack == grack) {
					continue;
				}
				if (areTooclose(posX, posY, grack.getWidth(), grack.getDepth(),
						grack.getAngle(), otherGrack.getX(), otherGrack.getY(),
						otherGrack.getWidth(), otherGrack.getDepth(),
						otherGrack.getAngle())) {
					posX = newPosX(grack.getWidth(), grack.getDepth(), grack
							.getAngle(), otherGrack.getX(), otherGrack.getY(),
							otherGrack.getWidth(), otherGrack.getDepth(),
							otherGrack.getAngle());
					modified = true;
					break;
				}
			}
		}
		grack.setX(posX);
		grack.setY(posY);
	}

	/** propagate the graphical modifications on the {@link Grid} elements */
	protected void propagateModifications() {
		for (final GRack grack : gracks) {
			grack.propagate();
		}
		for (final GServer gserver : gservers) {
			gserver.propagate();
		}
	}

	public void saveGridlib(final File file) {
		propagateModifications();
		GridConfigurationGenerator gcg;
		final String fileDir = file.getParentFile().getAbsolutePath();
		String fileName = file.getName();
		if (fileName.endsWith(".xml")) {
			fileName = fileName.substring(0, fileName.length() - 4);
		}
		try {
			System.out.println("saving file " + fileDir + "/" + fileName
					+ ".xml");
			gcg = new GridConfigurationGenerator(fileDir);
			grid.setName(fileName);
			gcg.generate(grid);
		} catch (final Exception e) {
			throw new RuntimeException("TODO: Handle this exception better", e);
		}
	}

	/** return a full line (with newline) representing a machine on entropy */
	public static String formatMachineToEntropy(Machine machine) {
		try {
			return machine.getName().replace(' ', '_') + ' '
					+ machine.getCpuNumber() + ' ' + machine.getCpuNumber()
					* machine.getCpuCapacity() + ' ' + machine.getMemory()
					+ '\n';
		} catch (RemoteException e) {
			throw new UnsupportedOperationException(
					"TODO : gotta catch it all ! ", e);
		}
	}

	public static String formatVMToEntropy(VM vm) {
		try {
			return vm.getName().replace(' ', '_') + ' ' + vm.getConsCpu() + ' '
					+ vm.getConsMemory() + '\n';
		} catch (Exception e) {
			throw new UnsupportedOperationException(
					"TODO : gotta catch it all ! ", e);
		}
	}

	public void saveEntropy(final File file) {
		propagateModifications();
		try {
			List<String> VMDescriptions = new ArrayList<String>();
			List<String> InitialConfig = new ArrayList<String>();
			FileWriter fw = new FileWriter(file);
			fw.write("\n#list of nodes\n");
			for (Site si : grid.getSites()) {
				for (Cluster cl : si.getClusters()) {
					for (Rack ra : cl.getRacks()) {
						for (Level le : ra.getLevels()) {
							Machine ma = le.getMachine();
							fw.write(formatMachineToEntropy(ma));
							String presentMachines = "";
							for (VM vm : ma.getVMs()) {
								presentMachines += ' ' + vm.getName();
								VMDescriptions.add(formatVMToEntropy(vm));
							}
							if (presentMachines.length() > 0) {
								InitialConfig.add(ma.getName()
										+ presentMachines + '\n');
							}
						}
					}
				}
			}
			fw.write("\n#list of VMs\n");
			for (String VMDesc : VMDescriptions) {
				fw.write(VMDesc);
			}
			fw.write("\n#initial configuration\n");
			for (String config : InitialConfig) {
				fw.write(config);
			}
			fw.close();
		} catch (Exception ex) {
			throw new UnsupportedOperationException(
					"TODO : catch this better:", ex);
		}
	}

	protected void selectNewRack(final GServer server) {
		for (final GRack rack : getGRacks()) {
			if (rack.containsPoint(server.getX(), server.getY(), 0)) {
				rack.addServer(server);
				return;
			} else {
			}
		}
	}

	public void selectServer(GServer describedServer) {
		List<Selectable> toAdd = new LinkedList<Selectable>();
		toAdd.add(describedServer);
		getHome().setSelectedItems(toAdd);
	}

	/** the map of each element to the color it had before applying last heatmap */
	protected HashMap<GElement, Integer> heatmapedElementsColors = new HashMap<GElement, Integer>();

	/** the last heatMap applied */
	protected DataMap dataMap = new DataMap();

	/**
	 * change the data map to use, and graphically present it ; nothing happens
	 * if the map is already the one used
	 */
	public void changeDataMap(DataMap map) {
		if (map == dataMap) {
			return;
		}
		clearDataMap();
		if (map == null) {
			map = new DataMap();
		}
		dataMap = map;
		applyDataMap();
	}

	/**
	 * force to show a new map with specified values for min and max<br />
	 * those values are typically dependant of the kind of the map we use.
	 */
	public void setDataMap(DataMap map, float minValue, float maxValue) {
		this.minValue = minValue;
		this.maxValue = maxValue;
		// this forces to reload the map
		dataMap = null;
		changeDataMap(map);
	}

	protected static final double DEFAULT_MIN_HEAT = 20;
	protected static final double DEFAULT_MAX_HEAT = 40;

	protected double minValue = DEFAULT_MIN_HEAT;
	protected double maxValue = DEFAULT_MAX_HEAT;

	protected void checkMinMaxVals() {
		if (minValue >= maxValue) {
			minValue = DEFAULT_MIN_HEAT;
			maxValue = DEFAULT_MAX_HEAT;
			for (ServerPoint point : dataMap.getServerPoints()) {
				double value = point.getValue();
				if (value < minValue) {
					minValue = value;
				}
				if (value > maxValue) {
					maxValue = value;
				}
			}
		}
	}

	/** apply the data map to the elements */
	protected void applyDataMap() {
		if (dataMap == null) {
			return;
		}
		checkMinMaxVals();
		for (ServerPoint point : dataMap.getServerPoints()) {
			boolean found = false;
			for (GServer server : gservers) {
				if (server.getName().equals(point.getServer())) {
					found = true;
					if (!heatmapedElementsColors.containsKey(server)) {
						heatmapedElementsColors.put(server, server.getColor());
					} else {
						System.err.println("error : server " + server
								+ " already contained in heatmappedelements");
					}
					server.setColor(value2colorRG(minValue, point.getValue(),
							maxValue));
					break;
				}
			}
			if (!found) {
				System.err.println("error : requested point "
						+ point.getServer() + "." + point.getPosition() + ":"
						+ point.getValue() + " could not be allocated");
			}
		}
	}

	/**
	 * remove any color modification applied to {@link GElement} in the home<br />
	 * this method is called before when any modification occurs to the heat
	 * map, so it is not required to be used for modifying elements
	 */
	public void clearDataMap() {
		for (Entry<GElement, Integer> entry : heatmapedElementsColors
				.entrySet()) {
			entry.getKey().setColor(entry.getValue());
		}
		heatmapedElementsColors.clear();
		dataMap = null;
	}

	/** set the lower bound of the range to use for heats and reload the map */
	public void setMinDataRange(double minValue) {
		clearDataMap();
		this.minValue = minValue;
		applyDataMap();
	}

	/** set the upper bound of the range to use for heats and reload the map */
	public void setMaxDataRange(double maxValue) {
		clearDataMap();
		this.maxValue = maxValue;
		applyDataMap();
	}

	public double getValueMinRange() {
		return minValue;
	}

	public double getValueMaxRange() {
		return maxValue;
	}

	/** @return an int between 0 and 255 from a float. Round it down, bound it */
	protected static int toByteRange(double value) {
		int val = (int) Math.floor(value);
		if (val > 255) {
			val = 255;
		}
		if (val < 0) {
			val = 0;
		}
		return val;
	}

	/**
	 * @return the color, in RGB format, to use on servers of specified value,
	 *         using {@link #minValue} and {@link #maxValue} to scale
	 */
	public static int value2colorRGB(float minValue, float value, float maxValue) {

		/** the scaled value : 0 is minValue(or less), 1 is maxvalue(or more) */
		float relative = (value - minValue) / (maxValue - minValue);
		int red = toByteRange(255 * (-1 + relative * 3));
		int green;
		if (relative < 0.6) {
			green = toByteRange(255 * 3 * relative);
		} else {
			green = toByteRange(255 * (3 - 3 * relative));
		}
		int blue = toByteRange(255 * (2 - relative * 6));
		int ret = red << 16 | green << 8 | blue;
		// System.out.println("input value : " + value + " r:" + red + ";g:"
		// + green + ";b:" + blue + " : total:" + ret);
		return ret;
	}

	public static int value2colorRG(double minValue, double value,
			double maxValue) {

		/** the scaled value : 0 is minValue(or less), 1 is maxvalue(or more) */
		double relative = (value - minValue) / (maxValue - minValue);
		int red = toByteRange(255 * relative * 2);
		int green = toByteRange(255 * (2 - 2 * relative));
		int blue = 0;
		int ret = red << 16 | green << 8 | blue;
		// System.out.println("input value : " + value + " r:" + red + ";g:"
		// + green + ";b:" + blue + " : total:" + ret);
		return ret;

	}

	/** computes the load map for CPU, and show it */
	public void showCpuMap() {
		MachineValuator valuator = new MachineValuator() {

			@Override
			public float evaluate(Machine machine) {
				float load = 0;
				try {
					for (VM vm : machine.getVMs()) {
						load += vm.getConsCpu() / machine.getCpuCapacity();
					}
				} catch (RemoteException e) {
					e.printStackTrace(System.err);
				}
				try {
					System.out.println(" machine " + machine.getName()
							+ " is evaluated for " + load);
				} catch (RemoteException e) {
					e.printStackTrace(System.err);
				}
				return load;
			}
		};
		DataMap map = loadMapFromGrid(valuator);
		setDataMap(map, 0, 1);
	}

	public void showMemMap() {
		MachineValuator valuator = new MachineValuator() {

			@Override
			public float evaluate(Machine machine) {
				float load = 0;
				try {
					for (VM vm : machine.getVMs()) {
						load += vm.getConsMemory() / machine.getMemory();
					}
				} catch (RemoteException e) {
					e.printStackTrace(System.err);
				} catch (IllegalLifeCycleException e) {
					e.printStackTrace(System.err);
				}
				try {
					System.out.println(" machine " + machine.getName()
							+ " is evaluated for " + load);
				} catch (RemoteException e) {
					e.printStackTrace(System.err);
				}
				return load;
			}
		};
		clearDataMap();
		DataMap map = loadMapFromGrid(valuator);
		setDataMap(map, 0, 1);

	}

	/** generates a given load map by using a specific valuator */
	protected DataMap loadMapFromGrid(MachineValuator mv) {
		DataMapFactory factory = new DataMapFactory();
		DataMap map = factory.createDataMap();
		try {
			for (GServer server : gservers) {
				Machine machine = server.getGridElement();
				float load = mv.evaluate(machine);
				map.getServerPoints().add(
						factory.createServerPoint(machine.getName(), load));
			}
		} catch (RemoteException e) {
			e.printStackTrace(System.err);
		}
		return map;
	}

	protected static interface MachineValuator {
		public float evaluate(Machine machine);
	}

	public Set<String> getModelsNames() {
		try {
			return grid.getKnownModelsNames();
		} catch (RemoteException e) {
			e.printStackTrace();
			return new HashSet<String>();
		}
	}

	public MachineModel getModelByName(String name) {
		try {
			return grid.getModelByName(name);
		} catch (RemoteException e) {
			e.printStackTrace();
			return null;
		}
	}
}
